/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { HvigorNode } from '@ohos/hvigor';
import {
  OhosAppContext,
  OhosHapContext,
  OhosHarContext,
  OhosHspContext,
  OhosPluginId,
  Target
} from '@ohos/hvigor-ohos-plugin';
import { ModuleBuildProfile } from '@ohos/hvigor-ohos-plugin/src/options/build/module-build-profile';
import { HMRouterPluginConfig, HMRouterPluginConfigParam } from './HMRouterPluginConfig';
import { HMRouterHvigorPlugin } from './HMRouterHvigorPlugin';
import { Logger } from './common/Logger';
import { RouterInfo } from './common/PluginModel';
import HMRouterPluginConstant from './constants/CommonConstants';
import TaskConstants from './constants/TaskConstants';
import HMFileUtil from './utils/FileUtil';
import ObfuscationUtil from './utils/ObfuscationUtil';
import PluginStore from './store/PluginStore';

/**
 * HMRouterPluginHandle class, used to handle the HMRouter plugin
 */
export class HMRouterPluginHandle {
  public readonly config: HMRouterPluginConfig;
  private readonly node: HvigorNode;
  private readonly moduleContext: OhosHapContext | OhosHarContext | OhosHspContext;
  private readonly appContext: OhosAppContext;
  private readonly plugin: HMRouterHvigorPlugin;

  // Constructor, initializes the node, module context, configuration, and plugin
  constructor(node: HvigorNode, moduleContext: OhosHapContext | OhosHarContext | OhosHspContext) {
    this.node = node;
    this.moduleContext = moduleContext;
    this.config = this.readConfig();
    this.appContext = this.node.getParentNode()?.getContext(OhosPluginId.OHOS_APP_PLUGIN);
    PluginStore.getInstance().projectFilePath = this.appContext.getProjectPath();
    this.plugin = new HMRouterHvigorPlugin(this.config);
  }

  // Start the plugin
  start() {
    Logger.info(
      `Exec ${this.moduleContext.getModuleType()}Plugin..., node:${this.node.getNodeName()}, nodePath:${this.node.getNodePath()}`
    );
    this.moduleContext.targets((target: Target) => {
      const targetName = target.getTargetName();
      this.node.registerTask({
        name: targetName + TaskConstants.PLUGIN_TASK,
        run: () => {
          this.taskExec();
          this.plugin.scanFiles = [];
          this.plugin.routerMap = [];
        },
        dependencies: [targetName + TaskConstants.PRE_BUILD],
        postDependencies: [targetName + TaskConstants.MERGE_PROFILE]
      });
      this.node.registerTask({
        name: targetName + TaskConstants.COPY_ROUTER_MAP_TASK,
        run: () => {
          this.copyRouterMapToRawFileTask(target.getBuildTargetOutputPath(), targetName);
          this.writeHspModuleName();
        },
        dependencies: [targetName + TaskConstants.PROCESS_ROUTER_MAP],
        postDependencies: [targetName + TaskConstants.PROCESS_RESOURCE]
      });
      this.node.registerTask({
        name: targetName + TaskConstants.GENERATE_OBFUSCATION_TASK,
        run: () => {
          this.generateObfuscationFileTask(this.moduleContext.getBuildProfileOpt());
        },
        dependencies: [targetName + TaskConstants.PLUGIN_TASK],
        postDependencies: [targetName + TaskConstants.MERGE_PROFILE]
      });
    });
  }

  // Generate the obfuscation configuration file
  private generateObfuscationFileTask(buildProfileOpt: ModuleBuildProfile.ModuleBuildOpt) {
    if (!this.isEnableObfuscation(buildProfileOpt)) {
      Logger.info(
        'This compilation does not turn on code obfuscation, skip hmrouter_obfuscation_rules.txt file generation');
      return;
    }
    let obfuscationFilePath = HMFileUtil.pathResolve(this.config.modulePath,
      HMRouterPluginConstant.OBFUSCATION_FILE_NAME);
    let routerMap = JSON.parse(HMFileUtil.readFileSync(this.config.getRouterMapDir()).toString()).routerMap;
    let obfuscationString = ObfuscationUtil.buildObfuscatedStrings(routerMap);
    HMFileUtil.ensureFileSync(obfuscationFilePath);
    HMFileUtil.writeFileSync(obfuscationFilePath, obfuscationString);
    if (this.moduleContext.getModuleType() === HMRouterPluginConstant.HAR_MODULE_NAME) {
      let consumerRulesPath = HMFileUtil.pathResolve(this.config.modulePath, HMRouterPluginConstant.CONSUMER_FILE_NAME);
      HMFileUtil.ensureFileSync(consumerRulesPath);
      HMFileUtil.writeFileSync(consumerRulesPath, obfuscationString);
    }
    Logger.info('Generate obfuscation rule file successfully, filePath:', obfuscationFilePath);
  }

  // Check if the current buildModel has obfuscation configuration enabled
  private isEnableObfuscation(buildProfileOpt: ModuleBuildProfile.ModuleBuildOpt): boolean {
    let currentBuildMode = this.appContext.getBuildMode();
    // Decide whether to register the obfuscation configuration task based on the obfuscation settings
    let buildOption = buildProfileOpt.buildOptionSet?.find((item) => {
      return item.name == currentBuildMode;
    });
    // Return false if the corresponding configuration is not found
    if (!buildOption) return false;
    let ruleOptions = this.ensureNestedObject(buildOption, ['arkOptions', 'obfuscation', 'ruleOptions']);
    // Automatically write the obfuscation file and enable obfuscation
    if (this.config.autoObfuscation && ruleOptions.enable) {
      // Building objects
      let files = this.ensureNestedObject(buildOption, ['arkOptions', 'obfuscation', 'ruleOptions', 'files']);
      let obfuscationFilePath = HMRouterPluginConstant.CURRENT_DELIMITER +
        HMRouterPluginConstant.OBFUSCATION_FILE_NAME;
      if (typeof files === 'string') {
        ruleOptions.files = [files, obfuscationFilePath];
      } else if (Array.isArray(files)) {
        files.push(obfuscationFilePath);
      } else {
        ruleOptions.files = obfuscationFilePath;
      }
      // If it's a har package, add the following logic
      if (this.moduleContext.getModuleType() === HMRouterPluginConstant.HAR_MODULE_NAME) {
        let consumerFiles = this.ensureNestedObject(buildOption, ['arkOptions', 'obfuscation', 'consumerFiles']);
        let consumerRulesPath = HMRouterPluginConstant.CURRENT_DELIMITER +
          HMRouterPluginConstant.CONSUMER_FILE_NAME;
        if (typeof consumerFiles === 'string') {
          this.ensureNestedObject(buildOption, ['arkOptions', 'obfuscation']).consumerFiles =
            [consumerFiles, consumerRulesPath];
        } else if (Array.isArray(consumerFiles)) {
          consumerFiles.push(consumerRulesPath);
        } else {
          this.ensureNestedObject(buildOption, ['arkOptions', 'obfuscation']).consumerFiles = consumerRulesPath;
        }
      }
      this.moduleContext.setBuildProfileOpt(buildProfileOpt);
    }
    return ruleOptions.enable;
  }

  // Copy the routerMap to the raw file
  private copyRouterMapToRawFileTask(buildOutputPath: string, targetName: string) {
    let routerMapFilePath = HMFileUtil.pathResolve(
      buildOutputPath,
      HMRouterPluginConstant.TEMP_ROUTER_MAP_PATH,
      targetName,
      HMRouterPluginConstant.ROUTER_MAP_NAME
    );
    let rawFilePath = this.config.getRawFilePath();
    HMFileUtil.ensureFileSync(rawFilePath);
    HMFileUtil.copyFileSync(routerMapFilePath, rawFilePath);
  }

  // Add the hsp package module name to the routerMap in the rawFile
  private writeHspModuleName() {
    if (!this.node.getAllPluginIds().includes(OhosPluginId.OHOS_HAP_PLUGIN)) return;
    let rawFilePath = this.config.getRawFilePath();
    let rawFileRouterMap = JSON.parse(HMFileUtil.readFileSync(rawFilePath).toString());
    rawFileRouterMap.hspModuleNames = [...new Set(PluginStore.getInstance().hspModuleNames)];
    rawFileRouterMap.hspModuleNames.push(...this.getRemoteHspModuleNames(this.node, this.node.getAllPluginIds()));
    HMFileUtil.writeFileSync(rawFilePath, JSON.stringify(rawFileRouterMap));
  }

  /**
   * @description get remote hsp module names
   * @param node current node
   * @param pluginIds Plugin ID array
   * @return string[] HSP module name array
   * @private
   */
  private getRemoteHspModuleNames(node: HvigorNode, pluginIds: string[]): string[] {
    let remoteHspModuleNames: string[] = [];
    try {
      pluginIds.forEach((id: string) => {
        let context = node.getContext(id);
        let signedHspObj = context.getOhpmRemoteHspDependencyInfo(true);
        let remoteHspObj = context.getOhpmRemoteHspDependencyInfo(false);
        for (const key in signedHspObj) {
          remoteHspModuleNames.push(signedHspObj[key].name);
        }
        for (const key in remoteHspObj) {
          remoteHspModuleNames.push(remoteHspObj[key].name);
        }
      });
    } catch (error) {
      Logger.warn('Your DevEco Studio version less than 5.0.3.800, may cause remote hsp dependencies get failed');
    }
    return remoteHspModuleNames;
  }

  // Execute tasks
  private taskExec() {
    let startTime = Date.now();
    Logger.info(this.node.getNodeName() + ': HMRouterPluginTask start');
    // Analysis Annotations
    this.plugin.analyzeAnnotation();
    // update module.json
    this.updateModuleJsonOpt();
    // update profile
    this.updateBuildProfileOpt();
    let endTime = Date.now();
    Logger.info(this.config.moduleName + ': HMRouterPluginTask end');
    Logger.info(this.config.moduleName + ': HMRouterPluginTask cost: ' + (endTime - startTime) + ' ms');
  }

  // update module.json
  private updateModuleJsonOpt() {
    // Get the JSON configuration of the module
    const moduleJsonOpt = this.moduleContext.getModuleJsonOpt();
    // If the JSON configuration of the module contains routerMap
    if (moduleJsonOpt.module.routerMap) {
      // Get the file name of routerMap
      let routerMapFileName = moduleJsonOpt.module.routerMap.split(':')[1];
      // Get the file path of routerMap
      let routerMapFilePath = this.config.getModuleRouterMapFilePath(routerMapFileName);
      // Read the JSON file of the routerMap
      let routerMapObj = HMFileUtil.readJson5(routerMapFilePath);
      // Add the routerMap to the plugin's routerMap
      if (routerMapFileName !== 'hm_router_map') {
        this.plugin.routerMap.unshift(
          ...((routerMapObj as any)[HMRouterPluginConstant.ROUTER_MAP_KEY] as Array<RouterInfo>));
      }
    }
    // generate routerMap
    this.plugin.generateRouterMap();
    // Set the name of the routerMap to MODULE_ROUTER_MAP_NAME
    moduleJsonOpt.module.routerMap = HMRouterPluginConstant.MODULE_ROUTER_MAP_NAME;
    // Set the modified moduleJsonOpt to the moduleContext
    this.moduleContext.setModuleJsonOpt(moduleJsonOpt);
  }

  // update build-profile
  private updateBuildProfileOpt() {
    // Get the build configuration options
    const buildProfileOpt = this.moduleContext.getBuildProfileOpt();
    // Initialize the arkOptions and runtimeOnly configurations
    let sources = this.ensureNestedObject(buildProfileOpt, ['buildOption', 'arkOptions', 'runtimeOnly', 'sources']);
    if (!Array.isArray(sources)) {
      buildProfileOpt.buildOption!.arkOptions!.runtimeOnly!.sources = [];
    }
    // Push the route information to the build configuration options
    this.pushRouterInfo(buildProfileOpt, this.plugin.routerMap);
    // Set the build configuration options to the moduleContext
    this.moduleContext.setBuildProfileOpt(buildProfileOpt);
  }

  // Add the route information to buildProfileOpt
  private pushRouterInfo(buildProfileOpt: any, routerMap: Array<RouterInfo>) {
    const sources = buildProfileOpt.buildOption.arkOptions.runtimeOnly.sources;
    routerMap.forEach((item: RouterInfo) => {
      const name = item.name;
      if (
        name.includes(HMRouterPluginConstant.LIFECYCLE_PREFIX) ||
        name.includes(HMRouterPluginConstant.INTERCEPTOR_PREFIX) ||
        name.includes(HMRouterPluginConstant.ANIMATOR_PREFIX) ||
        name.includes(HMRouterPluginConstant.SERVICE_PREFIX)
      ) {
        sources.push(HMRouterPluginConstant.CURRENT_DELIMITER + item.pageSourceFile);
      }
    });
  }

  // Read the configuration
  private readConfig(): HMRouterPluginConfig {
    let configParam: HMRouterPluginConfigParam;
    let configFilePath: string;
    let configDir: string;
    configFilePath = HMFileUtil.pathResolve(this.node.getNodePath(), HMRouterPluginConstant.CONFIG_FILE_NAME);
    if (!HMFileUtil.exist(configFilePath)) {
      // Indicates that there is no configuration under the module, read the project configuration
      configFilePath =
        HMFileUtil.pathResolve(PluginStore.getInstance().projectFilePath, HMRouterPluginConstant.CONFIG_FILE_NAME);
    }
    if (HMFileUtil.exist(configFilePath)) {
      configParam = HMFileUtil.readJson5(configFilePath);
    } else {
      configParam = {};
    }
    configDir = HMFileUtil.pathResolve(configFilePath, HMRouterPluginConstant.PARENT_DELIMITER);
    return new HMRouterPluginConfig(this.node.getNodeName(), this.node.getNodePath(), configDir, configParam);
  }

  /**
   * @description Ensure the nested property of the object exists
   * @param obj
   * @param path property path array
   * @return any, nested property value of the object
   */
  private ensureNestedObject(obj: any, path: string[]): any {
    return path.reduce((acc, key) => {
      if (!acc[key]) {
        acc[key] = {};
      }
      return acc[key];
    }, obj);
  }
}
